package server;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import server.Message.MsgType;

public class ConnectorThread extends Thread {

	private class ConnReq {
		private boolean me = true;
		
		private boolean inMesh = false;
		// private int responses;
		private int bestLoad;
		private SocketAddress bestAddr;

		private Message m;
	}

	private BlockingQueue<Message> uOutgoing;
	private ConcurrentHashMap<String, SocketAddress> uAddrs;
	private Map<String, Map<SocketAddress, Integer>> addrSeen;

	private BlockingQueue<Message> connections;
	private HashMap<String, ConnReq> idToConn;
	private HashMap<Long, String> timeToId;

	private PriorityQueue<Long> timers;

	public ConnectorThread(BlockingQueue<Message> connections, BlockingQueue<Message> uOutgoing,
			ConcurrentHashMap<String, SocketAddress> uAddrs,
			Map<String, Map<SocketAddress, Integer>> addrSeen) {
		this.uOutgoing = uOutgoing;
		this.uAddrs = uAddrs;
		this.addrSeen = addrSeen;

		this.connections = connections;
		this.idToConn = new HashMap<String, ConnReq>();
		this.timeToId = new HashMap<Long, String>();
		this.timers = new PriorityQueue<Long>();
	}

	private void log(String msg) {
		Server.log(this.getName(), msg);
	}

	@Override
	public void run() {
		while (true) {
			try {
				Long end = timers.peek();
				Message m = null;

				// if no timers to wait for, just wait until we get anything
				if (end == null)
					m = connections.take();
				else {
					// figure out if we have any time left to wait, if so, wait
					long left = end - System.currentTimeMillis();
					if (end > 0)
						m = connections.poll(left, TimeUnit.MILLISECONDS);
				}

				if (m != null) { // message to be processed
					switch (m.type) {
					case CONNECT:
						log("New connection request: " + m);
						String id;
						if ((m.flags & 0x1) > 0)
							id = Server.EMPTY_ID;
						else if (addrSeen.get(m.pubId) == null)
							id = Server.EMPTY_ID;
						else
							id = m.data;
						log("mesh: " + id);

						if (addrSeen.get(id) != null && addrSeen.get(id).size() == 0) { // nobody else to survey
							m.flags |= 0x2;
							if ((m.flags & 0x1) > 0) {
								addrSeen.put(m.pubId,
										new ConcurrentHashMap<SocketAddress, Integer>());
							}
							uOutgoing.put(m);
						}
						else if ((m.flags & 0x1) > 0 && uAddrs.size() == 0) {
							m.flags |= 0x2;
							addrSeen.put(m.pubId, new ConcurrentHashMap<SocketAddress, Integer>());
							uOutgoing.put(m);
						}
						else {
							ConnReq r = new ConnReq();
							r.m = m;
							idToConn.put(m.pubId, r);
							Long timer = System.currentTimeMillis() + 50;
							timeToId.put(timer, m.pubId);
							timers.add(timer);
							try {
								Message surveyMsg;
								if ((m.flags & 0x1) > 0)
									surveyMsg = new Message(MsgType.SURVEY, id, 0, m.pubId);
								else {
									surveyMsg = new Message(MsgType.SURVEY, id, 0, m.pubId + " " + m.data);
									
								}
								surveyMsg.flags = m.flags;
								if ((m.flags & 0x1) == 0 && addrSeen.get(m.data) != null && Server.calculateLoad(uAddrs.size()) > 0) {
									surveyMsg.flags |= 0x2; // let's everyone know i am in the mesh
									r.inMesh = true;
								}
								
								uOutgoing.put(surveyMsg);
							}
							catch (IOException e) {
								e.printStackTrace();
							}
						}
						break;
					case LOAD:
						ConnReq req = idToConn.get(m.pubId);
						if (req != null) {
							if ((m.flags & 0x1) != 0) {
								int load = Integer.parseInt(m.data);
								if (load > req.bestLoad) {
									log("L: " + load);
									req.bestLoad = load;
									req.bestAddr = m.addr;
									req.me = false;
								}
								else if (req.me && load > Server.calculateLoad(uAddrs.size())) {
									log("L: " + load);
									req.bestLoad = load;
									req.bestAddr = m.addr;
									req.me = false;	
								}
							}
							else {
								String[] args = m.data.split(" ");
								int load = Integer.parseInt(args[0]);
								if (req.inMesh && args.length == 1)
									break;
								else if (!req.inMesh && args.length > 1) {
									req.bestLoad = load;
									req.bestAddr = m.addr;
									req.me = false;
									req.inMesh = true;
									break;
								}
								else if (load > req.bestLoad) {
									log("L: " + load);
									req.bestLoad = load;
									req.bestAddr = m.addr;
									req.me = false;
								}
								else if (req.me && load > Server.calculateLoad(uAddrs.size())) {
									log("L: " + load);
									req.bestLoad = load;
									req.bestAddr = m.addr;
									req.me = false;	
								}
							}
						}
						break;
					default:
						break;
					}
				}
				else { // no message, a timer ran out
					log("Timer expired.");
					Long timer = timers.poll();
					String id = timeToId.remove(timer);
					ConnReq req = idToConn.remove(id);
					
					if (req == null)
						continue;

					if (req.me) {
						if (Server.calculateLoad(uAddrs.size()) <= 0)
							continue;
						
						req.m.flags |= 0x2;
						if ((req.m.flags & 0x1) > 0) {
							addrSeen.put(req.m.pubId,
									new ConcurrentHashMap<SocketAddress, Integer>());
						}
					}
					else {
						InetSocketAddress inetAddr = (InetSocketAddress) req.bestAddr;
						try {
							req.m.setData(inetAddr.getHostName() + " "
									+ (inetAddr.getPort() + 2000));
						}
						catch (UnsupportedEncodingException e) {
							e.printStackTrace();
						}
					}
					uOutgoing.put(req.m);
				}
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
